// 这个是cpp_110跟着老师上课敲的 带详细注释
//
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
using namespace std;

namespace mystring // 使用了命名空间 bit 来避免与标准库中的 std::string 冲突。
{
    class string
    {
    public:
        // 构造函数          (提供了一个缺省值表示在没有提供参数时，str 默认初始化为一个空字符串（即一个以 null 结尾的字符数组，其中只有一个字符 '\0'）)
        string(const char *str = "") // 这里是""  不是" "。后者不为空， 有一个空格
        {
            _size = strlen(str);
            _capacity = _size;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }

        // 析构函数
        ~string()
        {
            delete[] _str;
            _size = 0;
            _capacity = 0;
            _str = nullptr;
        }

        // 拷贝构造  s2(s1)  s2("Hello,world");

        // 传统写法1
        string(const string &s)
        {
            _str = new char[s._capacity + 1];
            strcpy(_str, s._str);
            _size = s._size;
            _capacity = s._capacity;
        }
        ////传统写法2
        // string(const string& s)
        //	:_str(nullptr) //初始化_str指针
        //	, _size(s._size) //复制s对象的_size大小
        //	, _capacity(s._size* (1.25)) //在当前_size大小的基础上多开1.25倍空间
        //{
        //	_str = new char[_capacity + 1]; //多开一位空间放\0
        //	strcpy(_str, s._str); //拷贝s字符串的字符数据
        // }

        // 现代写法  s2(s1)    下面的s 就是s1
        string(const string &s)
            : _str(nullptr), _size(0), _capacity(0)
        {
            string tmp(s._str); // 注意！是构造
                                // 使用 s 对象的内部 C 风格字符串 _str 构造一个临时的字符串对象 tmp
            swap(tmp);          // 交换当前对象和临时对象的数据，使当前对象的内容变为 tmp 的内容，临时对象则被销毁
        }
        // 解析：tmp和s1有一样大的空间，一样的值。然后s2和tmp一交换,那s2就和s1一样了，就完成了。

        // 赋值运算符重载
        // 传统写法 s1=s3
        string &operator=(const string &s)
        {
            if (this != &s) // 防止自己拷贝自己
            {
                char *tmp = new char[s._capacity + 1];
                strcpy(tmp, s._str);
                delete[] _str;
                _str = tmp;
                _size = s._size;
                _capacity = s._capacity;
            }
            return *this;
        }
        // 赋值运算符重载———现代写法   s1=s3,
        string &operator=(string s) // 注意是传值。传参的时候会复用拷贝构造
        {
            // 结合图看，s出了作用域会调用析构函数
            swap(s);
            return *this;
        }
        // 这个重载的赋值运算符的参数是一个 string 对象 s，而不是 const string& 。这意味着 s 是通过值传递给函数的，也就是说在函数内部会创建一个 s 的副本。然后在函数体内，通过 swap 函数，将这个副本的内容与当前对象的内容进行交换。由于参数是按值传递的，因此这个交换并不会影响到传入函数的原始对象 s，它仍然保持着它的原始内容。
        // 因此，无论如何，我们都会得到一个独立于原始对象的副本，这样就避免了深拷贝和自我赋值的问题。
        // 这种写法的优势在于简洁而且易读。它利用了 C++ 中的值传递机制，避免了显式的内存分配和释放，同时也不需要手动进行自我赋值检查，因为不论如何，我们都会获得一个独立的对象副本。
        // 这种技巧在 C++11 引入了移动语义之后变得更加普遍，被称为 "copy and swap" 策略，它简化了许多类的赋值运算符的实现。
        // 过程详解：
        //	s3 被拷贝构造，生成临时对象 temp，其内容与 s3 完全相同。
        //	在函数内部，调用 swap 函数，交换了 s1 和 temp 的 _str、_size 和 _capacity 成员。
        //	此时，s1 接管了 temp 中的资源，而 temp 中的资源实际上是属于 s3 的。
        //	随着函数结束，temp 被销毁，调用其析构函数释放了原本属于 s3 的资源。

        // 获取C风格字符串
        const char *c_str() const
        {
            return _str;
        }
        // 获取字符串长度
        size_t size() const
        {
            return _size;
        }
        // 获取当前容量
        size_t capacity()
        {
            return _capacity;
        }
        // const调const可以， 非const调const也可以，是权限的缩小
        // 以上三个 每个也可以弄两个版本，const和非const，但是没有必要，用一个就可以
        // 下面这个不行啊，有时候s[i]我们需要修改（访问+修改），有时候不需要修改（打印）,故需要两个版本

        bool empty() const
        {
            return _size == 0;
        }

        // reserve扩容
        void reserve(size_t n)
        {
            if (n > _capacity)
            {
                char *tmp = new char[n + 1]; // 空间永远比capacity多一个
                // 为了保证字符串的有效性和正确性，需要多开一个字符的空间，用于存放结尾的空字符 '\0'，以确保字符串的正确结束。
                strcpy(tmp, _str); // 也会拷贝\0
                delete[] _str;
                _str = tmp;
                _capacity = n;
            }
        }

        // resize调整字符串大小
        // void resize(size_t, char c = '\0');//声明 记得用缺省值,用户在调用 resize 函数时可以选择性地提供第二个参数
        // 对于函数声明定义分离实现，缺省参数写在声明中，定义时不需要写缺省参数！
        // 本文件的代码声明和定义在一起  就写上
        void resize(size_t n, char c)
        {
            if (n > _size)
            {
                if (n > _capacity)
                {
                    reserve(n);
                }
                for (size_t i = _size; i < n; i++)
                {
                    _str[i] = c;
                }
            }
            else if (n < _size)
            {
                _size = n;
                // 一定要记得 不会改变容量
            }
            _str[_size] = '\0';
        }

        // 字符串访问[]
        // 仅能访问
        const char &operator[](size_t pos) const
        {
            assert(pos < _size); /// assert 括号里为假的时候才会报错
            return _str[pos];
        }
        // 访问+修改
        char &operator[](size_t pos)
        {
            assert(pos <= _size);
            return _str[pos];
        }
        // 迭代器访问
        // 迭代器在 C++ 中常常被描述为类似指针的对象，它提供了对容器（比如字符串）中元素的访问和操作。
        // 对于模拟实现的字符串类，我们可以直接使用原生指针来作为迭代器，通过 typedef 进行重命名，这样就可以在类中直接使用迭代器。
        // 首先，我们使用 typedef 将指针重命名为迭代器，同时定义了常量迭代器：
        typedef char *iterator;
        typedef const char *const_iterator;
        // 返回字符串的起始位置
        iterator begin()
        {
            return _str;
        }

        // 返回字符串的结束位置'\0' 的下一个位置（即 null 字符的位置）
        iterator end()
        {
            return _str + _size;
        }

        // 返回字符串的起始位置（const 版本，不能修改数据）
        // 常量成员函数
        // 在你的代码中，const_iterator begin() const 和 const_iterator end() const 被声明为常量成员函数。这意味着它们不会修改对象的任何成员变量，并且它们可以被常量对象调用。
        // 为什么需要 const
        // 如果没有最后的 const 修饰符，编译器将认为 begin() 和 end() 可能会修改对象。因此，当你试图在一个常量对象上调用这些函数时，会产生编译错误，因为编译器不允许通过常量对象调用非常量成员函数。
        const_iterator begin() const
        {
            return _str;
        }

        // 返回字符串的结束位置的下一个位置（const 版本）
        const_iterator end() const
        {
            return _str + _size;
        }
        // 这些函数使得我们可以像操作指针一样操作迭代器，比如使用 ++ 和 -- 来移动迭代器指向的位置，或者使用 * 来访问迭代器指向的元素。
        // 这样，我们就可以通过迭代器来遍历字符串中的字符了。

        // 插入类

        // 尾插
        void push_back(char ch)
        {
            if (_size == _capacity) // 大小和总容量一样的时候，说明不够用了
            {
                size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
                reserve(newCapacity);
            }
            _str[_size] = ch;
            _size++;
            _str[_size] = '\0'; // 一定要处理好\0
        }
        // 尾插一个字符串
        void append(const char *str) // 注意传的是指针
        {
            size_t len = strlen(str);
            if (_size + len > _capacity)
            {
                reserve(_size + len);
            }
            // char *strcpy(char *dest, const char *src);   strcat(_str,str)也行，但是效率不行
            strcpy(_str + _size, str);
            _size += len;
        }
        // 尾插一个string类对象
        string &append(const string &str)
        {
            // 检查是否需要扩展容量
            if (str._size >= _capacity - _size) // 判断是否需要扩容
            {
                reserve(_capacity + str._size + 1); // 扩容并预留足够空间
            }

            // 复制传入的字符串到当前字符串的末尾
            strcpy(_str + _size, str._str); // _str + _size 是当前字符串尾部

            // 更新当前字符串的大小
            _size += str._size; // 更新_size

            // 手动设置字符串的结尾
            _str[_size] = '\0'; // 手动设置字符串尾部的\0

            // 返回当前对象的引用
            return *this; // 返回string对象
        }

        // 尾插n个字符c
        void append(size_t n, char ch)
        {
            // 检查是否需要扩展容量
            if (_size + n > _capacity)
            {
                reserve(_size + n); // 扩展容量以容纳新字符
            }

            // 将字符 ch 追加 n 次到字符串末尾
            for (size_t i = 0; i < n; i++)
            {
                _str[_size + i] = ch;
            }

            // 更新字符串的大小
            _size += n;

            // 确保字符串以 '\0' 结尾
            _str[_size] = '\0';
        }

        string &operator+=(char ch)
        {
            push_back(ch);
            return *this;
        }
        string &operator+=(const char *str)
        {
            append(str);
            return *this;
        }

        void insert(size_t pos, char ch)
        {
            assert(pos <= _size);   // 等于的时候就是尾插
            if (_size == _capacity) // 大小和总容量一样的时候，说明不够用了.我们之前多开了一个
            {
                size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
                reserve(newCapacity);
            }

            /*	size_t end = _size + 1;
                while (end > pos)
                {
                    _str[end] = _str[end - 1];
                    --end;
                }*/

            //	while (end >= pos)//  当一个有符号整数和一个无符号整数比较时，有符号整数会被转换为无符号整数。因此，如果有符号整数是负数，那么它会被转换为一个非常大的正数，然后再进行比较。
            // end 是 size_t 类型（无符号整数类型），而当 pos 为 0 时，end 也是无符号的，导致了 end >= pos 条件始终为真，从而导致了循环无限循环下去。
            // 这种情况下，在每一次循环中，end 会变得非常大，远大于 _size，从而导致数组访问越界。
            // end为-1的时候  -1 被转换为无符号整数会变成一个非常大的正整数，比如在一个 32 位系统上，可能会变成 4294967295（2^32 - 1）。
            int end = _size;
            while (end >= (int)pos)
            {
                _str[end + 1] = _str[end];
                --end;
            }

            _str[pos] = ch;
            _size++;
        }
        // 在 C++ 中，通常情况下，字符串的位置索引 pos 是从 0 开始的，即第一个字符的位置为 0，第二个为 1，依此类推。这种习惯是因为 C++ 中的数组和字符串的索引都是从 0 开始计数的。
        //_str 表示字符串的起始位置，即第一个字符的地址。
        //_str + 1 表示字符串中第二个字符的地址。
        //_str + pos 表示字符串中第 pos 个位置的地址，即要进行插入或其他操作的位置。

        void insert(size_t pos, const char *str)
        {
            assert(pos <= _size);
            size_t len = strlen(str);
            if (_size + len > _capacity)
            {
                reserve(_size + len);
            }
            int end = _size;
            while (end >= (int)pos)
            {
                _str[end + len] = _str[end];
                --end;
            }
            strncpy(_str + pos, str, len); // 用strncpy可以避免拷贝str的\0.

            _size += len;       // "xxxx" 只拷贝xxxx,不拷贝第五个(下标为4)的\0
            _str[_size] = '\0'; // 确保字符串以 '\0' 结尾
        }

        // 删除类
        // 声明：string& erase(size_t pos, size_t len = npos); 定义的时候不加缺省值
        void erase(size_t pos, size_t len) // pos 是下标，删除1个就是pos位置的那个
        {
            // assert(pos <_size);// xxxx size=4,
            // assert(_size > 0);
            assert(pos < _size);                   // 这里不需要检查 pos >= 0，因为 pos 是无符号类型
            if (len == npos || pos + len >= _size) // 要删完
            {                                      // 但我们不用删，直接缩大小，
                _str[pos] = '\0';
                _size = pos;
            }
            else
            { // 后面数据挪过去覆盖
                // hello,wordl
                //       ↑  ↑：pos+len
                //      pos   删3个
                strcpy(_str + pos, _str + pos + len);
                _size -= len; // 覆盖之后减少_size即可
            }
            //"abcdefghi"。假如pos是3,len是4。pos是下标
            //_str 指向字符串的第一个字符，即 'a'。
            //_str + pos 指向字符串的第 4 个字符，即 'd'。
            //_str + pos + len 指向字符串的第 8 个字符，即 'h'。
        }

        // 尽管标准库中的 std::swap 可以用于交换两个对象，但是它仅在你提供的交换操作对你特定类的成员变量的交换上不能直接进行。
        //  标准库的 std::swap 无法直接处理类的私有成员变量的交换，而必须通过类提供的接口进行交换操作。
        // 所以 自定义类型要自己写
        void swap(string &s)
        {
            std::swap(_str, s._str);
            std::swap(_size, s._size);
            std::swap(_capacity, s._capacity);
        }

        size_t find(char ch, size_t pos = 0) // 半缺省
        {
            for (size_t i = pos; i < _size; i++)
            {
                if (_str[i] == ch)
                {
                    return i;
                }
            }
            return npos;
        }

        size_t find(const char *str, size_t pos = 0)
        {
            const char *ptr = strstr(_str + pos, str);
            if (ptr == nullptr)
            {
                return npos;
            }
            else
            {
                return ptr - _str;
            }
        }
        // strstr 是 C 标准库中的一个函数，用于在一个字符串中查找另一个字符串的第一次出现。
        // 如果 strstr 找到子字符串，则返回一个指向第一次出现位置的指针；否则，返回 nullptr。

        string substr(size_t pos = 0, size_t len = npos)
        {

            assert(pos < _size);
            size_t end = pos + len;
            if (len == npos || pos + len >= _size)
            {
                end = _size;
            }
            string str;
            str.reserve(end - pos);
            for (size_t i = pos; i < end; i++)
            {
                str += _str[i];
            }
            return str; // 返回str的拷贝
        }

        // 打印C风格字符串。C风格字符串是以空字符 '\0' 结尾的字符数组
        void printf_str(const string &s) // 权限放大，上面的末尾要加const
                                         // 在C++中，成员函数末尾的const关键字用于指示该函数不会修改对象的状态。这种函数被称为const成员函数，它们对于保证对象的不可变性非常重要。
                                         // 在你的代码中，c_str() 和 size() 都是访问器函数，它们不会修改字符串对象的内容，因此应该声明为const成员函数以确保它们可以在const对象上调用。这样做不仅符合面向对象的设计理念，还允许用户在const对象上调用这些函数，以便于在const上下文中使用你的类。
                                         // 而对于const char& operator[](size_t pos) const，它是一个重载的下标运算符，用于访问字符串中指定位置的字符。由于该函数不会修改对象的内容，因此也应该声明为const成员函数。
        {
            for (size_t i = 0; i < s.size(); i++)
            {
                //	s[i]++;    参数加const 就是为了防止这里进行修改。
                cout << s[i] << " ";
            }
            cout << endl;
        }

        void printf_str(const string &s)
        {
            // 指向的内容不能修改，指针本身可以修改
            // s 是一个 const string& 类型的引用，因此你在使用 begin() 函数时得到的迭代器类型应该是 const_iterator，而不是普通的 iterator。
            string::const_iterator it = s.begin();
            while (it != s.end())
            {
                //*it++;
                cout << *it << " ";
                ++it;
            }
            cout << endl;
        }

        void clear()
        {
            _size = 0;
            _str[0] = '\0'; //
        }

    private:
        size_t _capacity = 0;
        size_t _size = 0;
        char *_str = nullptr;

        const static size_t npos = -1;
        // 在C++中，静态成员变量的定义通常需要在类的外部进行，而非静态成员变量则需要在类的内部进行定义。然而，对于静态成员变量如果其为 const 且为整数类型（包括枚举类型），则可以在类内部直接进行初始化。
        // 因此，对于 const static size_t npos = -1; 这样的声明，其允许在类内部直接进行初始化。
    };

    // 流插入（ << ）和流提取（ >> ）操作符的重载一般放在全局，而不是作为成员函数
    // 流插入
    ostream &operator<<(ostream &out, const string &s)
    {
        for (auto ch : s)
        {
            out << ch;
        }
        return out;
    }

    // 流体取(如果当前有内容，std是进行覆盖，我们这里加上clear之后才是覆盖，不然是尾插，因为s += ch; )
    istream &operator>>(istream &in, string &s)
    {
        s.clear();            // 清空当前字符串，以免变成尾插了
        char buff[128] = {0}; // 创建一个缓冲区用于暂存读取的字符序列
        char ch = in.get();   // 从输入流中读取一个字符
        int i = 0;            // 初始化缓冲区索引

        // 循环读取字符直到遇到空格或换行符
        while (ch != ' ' && ch != '\n')
        {
            buff[i++] = ch; // 将读取的字符存储到缓冲区中
            if (i == 127)   // 如果缓冲区即将满了
            {
                buff[i] = '\0'; // 在缓冲区末尾添加字符串终止符
                s += buff;      // 将缓冲区中的字符序列插入到字符串对象中
                i = 0;          // 重置缓冲区索引
            }
            ch = in.get(); // 读取下一个字符
        }

        // 处理剩余的字符序列
        if (i > 0)
        {
            buff[i] = '\0'; // 在缓冲区末尾添加字符串终止符
            s += buff;      // 将缓冲区中的字符序列插入到字符串对象中
        }

        return in; // 返回输入流对象的引用
    }

    void test_string1()
    {
        string s1("hello world");
        cout << s1.c_str() << endl;

        string s2;
        cout << s2.c_str() << endl;

        for (size_t i = 0; i < s1.size(); i++)
        {
            s1[i]++;
        }
        cout << s1.c_str() << endl;

        // 遍历
        string::iterator it = s1.begin();
        while (it != s1.end())
        {
            cout << *it << " ";
            ++it; // （相当于）指针++
        }
        cout << endl;

        // 傻瓜式的替换成迭代器()
        for (auto ch : s1)
        {
            cout << ch << " ";
        }
        cout << endl;
    }
    void test_string2()
    {

        string s1("hello,world");
        cout << s1.c_str() << endl;
        s1 += ' ';
        s1 += "NB";
        cout << s1.c_str() << endl;
        // cout << s1 << endl; 之所以会报错，是因为编译器不知道如何将 bit::string 类型的对象 s1 转换为 ostream 类型，这是由于缺乏与 ostream 的交互定义。
        // C++ 中的 cout 是 ostream 类型的对象，而它提供了对于标准输出流的输出操作符 << 的重载，以便于输出各种类型的数据。但是，对于自定义的类类型，如果要使用 cout 输出，你需要定义相应的输出操作符重载。
        /*	ostream 类定义了对于许多基本数据类型和标准库中的一些常用类型的输出操作符重载，使得你可以直接使用 cout 对它们进行输出，而无需手动定义输出操作符重载。一些常见的已经定义了输出操作符重载的类型包括：

                基本数据类型：int, float, double, char, bool, 等等。
                C 风格字符串：const char* ，即以 null 结尾的字符数组。
                标准库类型：例如 std::string, std::vector, std::list, std::map 等等。
                这些类型都可以直接使用 ostream 输出操作符 << 进行输出，而无需手动定义输出操作符重载。对于自定义的类型，如果想要使用 ostream 输出操作符 << 进行输出，就需要在类外部定义相应的输出操作符重载。*/
        s1.insert(5, 'X');
        cout << s1.c_str() << endl;
        s1.insert(0, 'X');
        cout << s1.c_str() << endl;
        s1.insert(1, "lll");
        cout << s1.c_str() << endl;
    }
    void test_string3()
    {
        string s1("abcdefghi");
        s1.erase(5, 100);
        cout << s1.c_str() << endl;

        string s2("ABCDE");
        s2.erase(0, 10000);
        cout << s2.c_str() << endl;
    }
    void test_string4()
    {
        string s1("hello,world");
        string s2("xxx");
        std::swap(s1, s2);
        s1.swap(s2);
        cout << s1.c_str() << endl;
        cout << s2.c_str() << endl;

        string str("https://legacy.cplusplus.com/reference/string/string/substr/");
        string sub1, sub2, sub3;
        size_t pos1 = str.find(':');
        sub1 = str.substr(0, pos1 - 0); // 默认调用拷贝构造，但没写，所以默认浅拷贝。出了作用域，str调用析构，把那块空间带走了，但是临时对象还是指向那一块空间，str就成了野指针
        // 当你调用 string sub1 = str.substr(0, pos1 - 0); 时，substr 返回了一个临时的 string 对象 sub1，它包含了从 str 中提取的子串。这个临时对象是通过调用 string 类的构造函数生成的。

        //	默认情况下，如果你没有显式定义拷贝构造函数，C++ 编译器会为你生成一个默认的浅拷贝构造函数。浅拷贝构造函数会简单地复制对象中的成员变量，这就意味着新对象和原始对象指向相同的内存空间。

        //	所以，当 sub1 被构造时，它的 _str 指针与 str 对象的 _str 指针指向相同的内存空间，即 str 对象中存储字符串的内存空间。

        //	当 sub1 超出了其作用域时（例如函数结束），它的析构函数会被调用，而这个析构函数会释放 sub1 对象所持有的内存空间。

        //	现在问题出在 str 对象的 _str 指针指向的内存空间已经被释放了，因为它与 sub1 共享相同的内存空间。这就导致了 str 对象成为了一个悬空指针，任何对它的访问都会导致未定义的行为，这就是所谓的野指针问题。

        //	解决这个问题的方法是在 substr 函数中显式地定义一个拷贝构造函数，确保在创建 sub1 时会复制字符串的内容而不是共享相同的内存空间。可以通过深拷贝来实现，即在堆上为新的字符串分配内存空间，并将原始字符串的内容复制到新的内存空间中。这样，在 sub1 超出作用域后，它的析构函数释放的是它自己所持有的内存空间，而不会影响原始字符串 str。

        cout << sub1.c_str() << endl;

        size_t pos2 = str.find('/', pos1 + 3);
        sub2 = str.substr(pos1 + 3, pos2 - (pos1 + 3));
        cout << sub2.c_str() << endl;

        sub3 = str.substr(pos2 + 1);
        cout << sub3.c_str() << endl;
    }
    void test_string5()
    {
        string s1("hello world");
        string s2(s1);
        //	首先，string s2(s1); 这行代码会调用 string 类的复制构造函数。由于你没有显式定义复制构造函数，编译器会生成一个默认的浅拷贝构造函数。

        //		默认的浅拷贝构造函数会简单地复制对象中的成员变量。对于 string 类来说，其中包含了 _capacity、_size 和 _str 三个成员变量。在浅拷贝中，_str 指针的值会被简单地复制到新对象中，也就是说，s2 和 s1 的 _str 成员指向相同的内存空间。

        //		当 s1 超出作用域时，它的析构函数会被调用，析构函数会释放 s1 所持有的内存空间，包括其中存储的字符串内容。由于 s2 的 _str 指向相同的内存空间，当 s1 的析构函数释放内存后，s2 的 _str 就成了悬空指针，指向了被释放的内存空间。

        //		当你尝试访问 s2 中的字符串内容时，由于 _str 指向的内存已经被释放，就会导致程序崩溃或未定义的行为。

        //		为了避免这个问题，你可以通过显式定义 string 类的复制构造函数，实现深拷贝。深拷贝会在堆上为新的字符串分配内存空间，并将原始字符串的内容复制到新的内存空间中，而不是简单地复制指针。这样，在原始字符串被释放后，新对象仍然可以安全地访问其字符串内容。
        // string s3("xxxx");
        // s1 = s3;
        // string s4, s5;
        // cin >> s4 >> s5;
        // cout << s4 << endl << s5;
        cin >> s1;
        cout << s1 << endl; // 输入1234  567 只会提取到1234，除非你再次提取
        string s6;
        cin >> s6;
        cout << s6;
    }
    void test_string6()
    {
        string s1("hello world");
        string s2(s1);
        cout << s1 << endl;
        cout << s2 << endl;
        string s3("xxxxx");
        s1 = s3;
        cout << s1 << endl;
        cin >> s1;
        cout << s1 << endl;
    }
    void test_string8()
    {
        string s1("Hello,world"); // 调用拷贝构造
        string s2(s1);            // 调用拷贝构造
    }
}
